/* Copyright (C) 1999-2004 by Peter Eastman

   This program is free software; you can redistribute it and/or modify it under the
   terms of the GNU General Public License as published by the Free Software
   Foundation; either version 2 of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful, but WITHOUT ANY 
   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 
   PARTICULAR PURPOSE.  See the GNU General Public License for more details. */

package artofillusion;

import artofillusion.math.*;
import artofillusion.object.*;
import artofillusion.ui.*;
import artofillusion.view.*;
import buoy.event.*;
import buoy.widget.*;
import java.awt.*;
import java.awt.image.*;
import java.io.*;

/** ViewerCanvas is the abstract superclass of all components which display objects, and allow
    the user to manipulate them with EditingTools. */

public abstract class ViewerCanvas extends CustomWidget
{
  protected Image theImage;
  protected Camera theCamera;
  protected ObjectInfo boundCamera;
  protected EditingTool currentTool, activeTool, metaTool, altTool;
  protected BComboBox viewChoice, perspectiveChoice;
  protected ValueField scaleField;
  protected PopupMenuManager popupManager;
  protected int renderMode, gridSubdivisions, pixel[], zbuffer[];
  protected double gridSpacing;
  protected Graphics gr;
  protected boolean hideBackfaces, showGrid, snapToGrid, drawFocus, showTemplate, showAxes;
  protected ActionProcessor mouseProcessor;
  protected Image templateImage;
  protected int templatePixel[];
  
  private static Vec2 reuseVec2[];
  
  public static final int RENDER_WIREFRAME = 0;
  public static final int RENDER_FLAT = 1;
  public static final int RENDER_SMOOTH = 2;
  public static final int RENDER_TEXTURED = 3;
  public static final int RENDER_TRANSPARENT = 4;

  public ViewerCanvas()
  {
    CoordinateSystem coords = new CoordinateSystem(new Vec3(0.0, 0.0, ModellingApp.DIST_TO_SCREEN), new Vec3(0.0, 0.0, -1.0), Vec3.vy());

    theCamera = new Camera();
    theCamera.setCameraCoordinates(coords);
    setBackground(Color.white);
    hideBackfaces = true;
    setFocusable(true);
    addEventLink(RepaintEvent.class, this, "paint");
    addEventLink(MousePressedEvent.class, this, "processMousePressed");
    addEventLink(MouseReleasedEvent.class, this, "processMouseReleased");
    addEventLink(MouseDraggedEvent.class, this, "processMouseDragged");
    addEventLink(MouseMovedEvent.class, this, "processMouseDragged"); // Workaround for Mac OS X bug
    if (reuseVec2 == null)
    {
      reuseVec2 = new Vec2 [10000];
      for (int i = 0; i < reuseVec2.length; i++)
        reuseVec2[i] = new Vec2();
    }
  }

  /** Build the menus at the top.  Subclasses of ViewerCanvas can call this method if they want to
      use the standard set of menus. */

  protected void buildChoices(RowContainer row)
  {
    row.add(viewChoice = new BComboBox(new String [] {
      Translate.text("Front"),
      Translate.text("Back"),
      Translate.text("Left"),
      Translate.text("Right"),
      Translate.text("Top"),
      Translate.text("Bottom"),
      Translate.text("Other"),
    }));
    viewChoice.setSelectedIndex(0);
    viewChoice.addEventLink(ValueChangedEvent.class, this, "choiceChanged");
    row.add(perspectiveChoice = new BComboBox(new String [] {
      Translate.text("Perspective"),
      Translate.text("Parallel")
    }));
    perspectiveChoice.setSelectedIndex(1);
    perspectiveChoice.addEventLink(ValueChangedEvent.class, this, "choiceChanged");
    row.add(scaleField = new ValueField(100.0, ValueField.POSITIVE, 5));
    scaleField.setText("100");
    scaleField.setMinDecimalPlaces(1);
    scaleField.addEventLink(ValueChangedEvent.class, this, "scaleChanged");
  }
  
  private void processMousePressed(WidgetMouseEvent ev)
  {
    if (mouseProcessor != null)
      mouseProcessor.stopProcessing();
    mousePressed(ev);
    mouseProcessor = new ActionProcessor();
  }
  
  private void processMouseDragged(final WidgetMouseEvent ev)
  {
    if (mouseProcessor != null)
      mouseProcessor.addEvent(new Runnable() {
        public void run()
        {
          mouseDragged(ev);
        }
      });
  }
  
  private void processMouseReleased(WidgetMouseEvent ev)
  {
    if (mouseProcessor != null)
    {
      mouseProcessor.stopProcessing();
      mouseProcessor = null;
      mouseReleased(ev);
    }
  }
  
  /** Subclasses should override this to handle events. */
  
  protected void mousePressed(WidgetMouseEvent ev)
  {
  }
  
  /** Subclasses should override this to handle events. */

  protected void mouseDragged(WidgetMouseEvent ev)
  {
  }
  
  /** Subclasses should override this to handle events. */

  protected void mouseReleased(WidgetMouseEvent ev)
  {
  }
  
  /** Get the ActionProcessor which is currently in use for processing mouse events (may be null). */
  
  public ActionProcessor getActionProcessor()
  {
    return mouseProcessor;
  }

  public Camera getCamera()
  {
    return theCamera;
  }

  /** Set the currently selected tool. */

  public void setTool(EditingTool tool)
  {
    currentTool = tool;
    updateImage();
    repaint();
  }
  
  /** Get the currently selected tool. */
  
  public EditingTool getCurrentTool()
  {
    return currentTool;
  }

  /** Set the tool which should be active when the meta key is pressed. */
  
  public void setMetaTool(EditingTool tool)
  {
    metaTool = tool;
  }

  /** Set the tool which should be active when the alt key is pressed. */
  
  public void setAltTool(EditingTool tool)
  {
    altTool = tool;
  }
  
  /** Set whether to display perspective or parallel mode. */

  public void setPerspective(boolean perspective)
  {
    perspectiveChoice.setSelectedIndex(perspective ? 0 : 1);
    scaleField.setEnabled(!perspective);
  }
  
  /** Determine whether the view is currently is perspective mode. */
  
  public boolean isPerspective()
  {
    return (perspectiveChoice.getSelectedIndex() == 0);
  }
  
  /** Get the current scale factor for the view. */
  
  public double getScale()
  {
    if (isPerspective())
      return 100.0;
    return scaleField.getValue();
  }
  
  /** Set the scale factor for the view. */
  
  public void setScale(double scale)
  {
    if (scale > 0.0)
      scaleField.setValue(scale);
  }
  
  /** Set whether a focus ring should be drawn around this component. */
  
  public void setDrawFocus(boolean draw)
  {
    drawFocus = draw;
  }
  
  /** Determine whether the coordinate axes are currently showing. */
  
  public boolean getShowAxes()
  {
    return showAxes;
  }
  
  /** Set whether the coordinate axes should be displayed. */
  
  public void setShowAxes(boolean show)
  {
    showAxes = show;
  }
  
  /** Determine whether the template image is currently showing. */
  
  public boolean getTemplateShown()
  {
    return showTemplate;
  }
  
  /** Set whether the template image should be displayed. */
  
  public void setShowTemplate(boolean show)
  {
    showTemplate = show;
  }
  
  /** Get the template image. */
  
  public Image getTemplateImage()
  {
    return templateImage;
  }
  
  /** Set the template image. */
  
  public void setTemplateImage(Image im) throws InterruptedException
  {
    templateImage = im;
    PixelGrabber pg = new PixelGrabber(im, 0, 0, -1, -1, true);
    pg.grabPixels();
    templatePixel = (int []) pg.getPixels();
  }
  
  /** Set the template image based on an image file. */
  
  public void setTemplateImage(File f) throws InterruptedException
  {
    Image im = Toolkit.getDefaultToolkit().getImage(f.getAbsolutePath());
    MediaTracker mt = new MediaTracker(getComponent());
    mt.addImage(im, 0);
    mt.waitForID(0);
    if (mt.isErrorID(0))
      throw (new InterruptedException());
    setTemplateImage(im);
  }
  
  /** Set the PopupMenuManager for this canvas. */

  public void setPopupMenuManager(PopupMenuManager manager)
  {
    popupManager = manager;
  }
  
  /** Display the popup menu when an appropriate event occurs. */
  
  protected void showPopupIfNeeded(WidgetMouseEvent ev)
  {
    if (!ev.isPopupTrigger() || popupManager == null)
      return;
    Point pos = ev.getPoint();
    popupManager.showPopupMenu(this, pos.x, pos.y);
  }

  public synchronized void paint(RepaintEvent ev)
  {
    Graphics g = ev.getGraphics();
    Rectangle bounds = getBounds();
    if (theImage == null || theImage.getWidth(null) != bounds.width || theImage.getHeight(null) != bounds.height)
      updateImage();
    g.drawImage(theImage, 0, 0, null);
    currentTool.drawOverlay(g, this);
//    Toolkit.getDefaultToolkit().sync();
  }

  public void drawImage(Graphics g)
  {
    Rectangle bounds = getBounds();
    if (theImage == null || theImage.getWidth(null) != bounds.width || theImage.getHeight(null) != bounds.height)
      updateImage();
    g.drawImage(theImage, 0, 0, null);
  }

  public void adjustCamera(boolean perspective)
  {
    Rectangle bounds = getBounds();
    double scale = getScale();

    if (perspective)
      theCamera.setScreenParams(0, scale, bounds.width, bounds.height);
    else
      theCamera.setScreenParamsParallel(scale, bounds.width, bounds.height);  
  }
  
  /** Get the SceneCamera (if any) which is bound to this view. */
  
  public ObjectInfo getBoundCamera()
  {
    return boundCamera;
  }

  /** Set the grid parameters for this view. */

  public void setGrid(double spacing, int subdivisions, boolean show, boolean snap)
  {
    gridSpacing = spacing;
    gridSubdivisions = subdivisions;
    showGrid = show;
    snapToGrid = snap;
    if (snap)
      theCamera.setGrid(spacing/subdivisions);
    else
      theCamera.setGrid(0.0);
  }
  
  /** Adjust the camera position and magnification so that the specified box
      fills the view.  This has no effect if there is a camera point to this
      view. */
  
  public void frameBox(BoundingBox bb)
  {
    if (boundCamera != null)
      return;
    
    // Move the camera so that it points at the center of the box, and is well outside it. 
       
    Rectangle bounds = getBounds();
    if (perspectiveChoice.getSelectedIndex() == 0)
      theCamera.setScreenParams(0, 100.0, bounds.width, bounds.height);
    else
      theCamera.setScreenParamsParallel(100.0, bounds.width, bounds.height);  
    double startDist = ModellingApp.DIST_TO_SCREEN+Math.max(Math.max(bb.maxx-bb.minx, bb.maxy-bb.miny), bb.maxz-bb.minz);
    CoordinateSystem coords = theCamera.getCameraCoordinates();
    Vec3 boxCenter = bb.getCenter();
    coords.setOrigin(boxCenter.minus(coords.getZDirection().times(startDist)));
    theCamera.setCameraCoordinates(coords);
    
    // Now adjust the magnification or camera position to make the box fill
    // the view.
    
    theCamera.setObjectTransform(Mat4.identity());
    Rectangle screenBounds = theCamera.findScreenBounds(bb);
    double scalex = bounds.width/(double) screenBounds.width;
    double scaley = bounds.height/(double) screenBounds.height;
    double scale = (scalex < scaley ? scalex : scaley);
    if (perspectiveChoice.getSelectedIndex() == 0)
    {
      // Perspective mode, so adjust the camera position.
      
      coords.setOrigin(boxCenter.minus(coords.getZDirection().times(1.1*startDist/scale)));
      scaleField.setValue(100.0);
    }
    else
    {
      // Parallel mode, so adjust the magnification.
      
      scaleField.setValue(scale*100.0);
    }
    theCamera.setCameraCoordinates(coords);

  }
  
  /** Subclasses should override this to draw the contents of the canvas, but should begin
      by calling super.updateImage() to make sure the Image exists and is the right size. */

  public synchronized void updateImage()
  {
    Rectangle bounds = getBounds();
    
    if (bounds.height <= 0)
      return;
    if (boundCamera != null)
    {
      boundCamera.coords.copyCoords(theCamera.getCameraCoordinates());
      theCamera.setDistToScreen((bounds.height/200.0)/Math.tan(((SceneCamera) boundCamera.object).getFieldOfView()*Math.PI/360.0));
    }
    else
      theCamera.setDistToScreen(ModellingApp.DIST_TO_SCREEN);
    if (theImage == null || theImage.getWidth(null) != bounds.width || theImage.getHeight(null) != bounds.height)
    {
      if (gr != null)
        gr.dispose();
      if (bounds.width < 0 || bounds.height < 0)
        bounds.width = bounds.height = 0;
      theImage = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_INT_ARGB_PRE);
      gr = theImage.getGraphics();
      pixel = ((DataBufferInt) ((BufferedImage) theImage).getRaster().getDataBuffer()).getData();
      if (renderMode != RENDER_WIREFRAME)
        zbuffer = new int [bounds.width*bounds.height];
    }
    if (renderMode == RENDER_WIREFRAME)
    {
      gr.setColor(Color.white);
      gr.fillRect(0, 0, bounds.width, bounds.height);
    }
    else
      for (int i = 0; i < pixel.length; i++)
      {
        pixel[i] = 0xFFFFFFFF;
        zbuffer[i] = Integer.MAX_VALUE;
      }
    
    // Draw the template image, if necessary.
    
    if (showTemplate)
    {
      if (renderMode == RENDER_WIREFRAME)
        gr.drawImage(templateImage, 0, 0, null);
      else
      {
        int width = templateImage.getWidth(null), height = templateImage.getHeight(null);
        int maxi = (height < bounds.height ? height : bounds.height);
        int maxj = (width < bounds.width ? width : bounds.width);
        int frombase = 0, tobase = 0;
        for (int i = 0; i < maxi; i++)
          System.arraycopy(templatePixel, width*i, pixel, bounds.width*i, maxj);
      }
    }
    
    // Draw the grid, if necessary.
    
    if (showGrid)
    {
      if (perspectiveChoice.getSelectedIndex() == 1)
      {
        // Parallel mode, so draw a flat grid.
        
        Vec2 v1 = theCamera.getViewToScreen().timesXY(new Vec3());
        Vec2 v2 = theCamera.getViewToScreen().timesXY(new Vec3(gridSpacing, gridSpacing, 1.0));
        Vec2 v3 = theCamera.getWorldToScreen().timesXY(new Vec3());
        double space = Math.abs(v1.x-v2.x);
        int origin = (int) v3.x;
        double pos = Math.IEEEremainder(v3.x, space);
        if (pos < 0.0)
          pos += space;
        while ((int) pos < bounds.width)
        {
          int x = (int) pos;
          drawVRule(x, (x == origin ? Color.darkGray : Color.lightGray));
          pos += space;
        }
        space = Math.abs(v1.y-v2.y);
        origin = (int) v3.y;
        pos = Math.IEEEremainder(v3.y, space);
        if (pos < 0.0)
          pos += space;
        while ((int) pos < bounds.height)
        {
          int y = (int) pos;
          drawHRule(y, (y == origin ? Color.darkGray : Color.lightGray));
          pos += space;
        }
      }
      else
      {
        // Perspective mode, so draw a ground plane.
        
        theCamera.setObjectTransform(Mat4.identity());
        int size = (int) Math.max(10, 10/gridSpacing);
        for (int i = -size; i <= size; i++)
        {
          Vec3 end1 = new Vec3(i*gridSpacing, 0.0, -size*gridSpacing);
          Vec3 end2 = new Vec3(i*gridSpacing, 0.0, size*gridSpacing);
          renderLine(end1, end2, theCamera, i == 0 ? Color.darkGray : Color.lightGray);
        }
        for (int i = -size; i <= size; i++)
        {
          Vec3 end1 = new Vec3(-size*gridSpacing, 0.0, i*gridSpacing);
          Vec3 end2 = new Vec3(size*gridSpacing, 0.0, i*gridSpacing);
          renderLine(end1, end2, theCamera, i == 0 ? Color.darkGray : Color.lightGray);
        }
      }
    }
  }
  
  /** Draw the coordinate axes into the view. */
  
  protected void drawCoordinateAxes()
  {
    // Select a size for the coordinate axes.
    
    Rectangle bounds = getBounds();
    int axisLength = 50;
    if (axisLength*5 > bounds.width)
      axisLength = bounds.width/5;
    if (axisLength*5 > bounds.height)
      axisLength = bounds.height/5;
    double len = axisLength/getScale();
    
    // Calculate the screen positions of the axis ends.
    
    Vec2 offset = new Vec2(0.5*bounds.width-axisLength-15, 0.5*bounds.height-axisLength-15);
    CoordinateSystem cameraCoords = theCamera.getCameraCoordinates();
    Vec3 center = cameraCoords.getOrigin().plus(cameraCoords.getZDirection().times(theCamera.getDistToScreen()));
    Vec3 xpos = center.plus(new Vec3(len, 0.0, 0.0));
    Vec3 ypos = center.plus(new Vec3(0.0, len, 0.0));
    Vec3 zpos = center.plus(new Vec3(0.0, 0.0, len));
    Vec2 screenCenter = theCamera.getWorldToScreen().timesXY(center).plus(offset);
    Vec2 screenX = theCamera.getWorldToScreen().timesXY(xpos).plus(offset);
    Vec2 screenY = theCamera.getWorldToScreen().timesXY(ypos).plus(offset);
    Vec2 screenZ = theCamera.getWorldToScreen().timesXY(zpos).plus(offset);
    
    // Draw the axes.
    
    Point centerPoint = new Point((int) Math.round(screenCenter.x), (int) Math.round(screenCenter.y));
    drawLine(centerPoint, new Point((int) screenX.x, (int) screenX.y), Color.black);
    drawLine(centerPoint, new Point((int) screenY.x, (int) screenY.y), Color.black);
    drawLine(centerPoint, new Point((int) screenZ.x, (int) screenZ.y), Color.black);
    
    // Draw the labels.
    
    if (screenX.minus(screenCenter).length() > 2.0)
    {
      Vec2 dir = screenX.minus(screenCenter);
      Vec2 labelPos = screenX.plus(dir.times(8.0/dir.length()));
      int x = (int) labelPos.x;
      int y = (int) labelPos.y;
      drawLine(new Point(x-4, y-4), new Point(x+4, y+4), Color.black);
      drawLine(new Point(x-4, y+4), new Point(x+4, y-4), Color.black);
    }
    if (screenY.minus(screenCenter).length() > 2.0)
    {
      Vec2 dir = screenY.minus(screenCenter);
      Vec2 labelPos = screenY.plus(dir.times(8.0/dir.length()));
      int x = (int) labelPos.x;
      int y = (int) labelPos.y;
      drawLine(new Point(x-4, y-4), new Point(x, y), Color.black);
      drawLine(new Point(x+4, y-4), new Point(x, y), Color.black);
      drawLine(new Point(x, y), new Point(x, y+4), Color.black);
    }
    if (screenZ.minus(screenCenter).length() > 2.0)
    {
      Vec2 dir = screenZ.minus(screenCenter);
      Vec2 labelPos = screenZ.plus(dir.times(8.0/dir.length()));
      int x = (int) labelPos.x;
      int y = (int) labelPos.y;
      drawLine(new Point(x-4, y-4), new Point(x+4, y-4), Color.black);
      drawLine(new Point(x+4, y-4), new Point(x-4, y+4), Color.black);
      drawLine(new Point(x-4, y+4), new Point(x+4, y+4), Color.black);
    }
  }
  
  public int getRenderMode()
  {
    return renderMode;
  }
  
  public void setRenderMode(int mode)
  {
    renderMode = mode;
    theImage = null;
  }
  
  /** Adjust the coordinates of a mouse event to move it to the nearest
      grid location. */
  
  void moveToGrid(WidgetMouseEvent e)
  {
    Point pos = e.getPoint();
    Vec3 v;
    Vec2 v2;
    
    if (!snapToGrid || perspectiveChoice.getSelectedIndex() == 0)
      return;
    v = theCamera.convertScreenToWorld(pos, theCamera.getDistToScreen());
    v2 = theCamera.getWorldToScreen().timesXY(v);
    e.translatePoint((int) v2.x - pos.x, (int) v2.y - pos.y);
  }

  /** Deal with the various choice menus. */
  
  protected void choiceChanged(WidgetEvent ev)
  {
    if (ev.getWidget() == viewChoice)
      selectOrientation(viewChoice.getSelectedIndex());
    scaleField.setEnabled(!isPerspective());
    updateImage();
    repaint();
  }
  
  /** Deal with changes to the scale. */
  
  protected void scaleChanged()
  {
    updateImage();
    repaint();
  }

  /** Set the view orientation to any of the values shown in the choice menu. */
  
  public void selectOrientation(int which)
  {
    if (viewChoice.getSelectedIndex() != which)
      viewChoice.setSelectedIndex(which);
    CoordinateSystem coords = theCamera.getCameraCoordinates();
    boolean perspective = (perspectiveChoice.getSelectedIndex() == 0);
    double dist = (perspective ? coords.getOrigin().length() : ModellingApp.DIST_TO_SCREEN);
        
    if (which == 0)                // Front
      coords = new CoordinateSystem(new Vec3(0.0, 0.0, dist), new Vec3(0.0, 0.0, -1.0), Vec3.vy());
    else if (which == 1)        // Back
      coords = new CoordinateSystem(new Vec3(0.0, 0.0, -dist), Vec3.vz(), Vec3.vy());
    else if (which == 2)        // Left
      coords = new CoordinateSystem(new Vec3(-dist, 0.0, 0.0), Vec3.vx(), Vec3.vy());
    else if (which == 3)        // Right
      coords = new CoordinateSystem(new Vec3(dist, 0.0, 0.0), new Vec3(-1.0, 0.0, 0.0), Vec3.vy());
    else if (which == 4)        // Top
      coords = new CoordinateSystem(new Vec3(0.0, dist, 0.0), new Vec3(0.0, -1.0, 0.0), new Vec3(0.0, 0.0, -1.0));
    else if (which == 5)        // Bottom
      coords = new CoordinateSystem(new Vec3(0.0, -dist, 0.0), Vec3.vy(), Vec3.vz());
    theCamera.setCameraCoordinates(coords);
  }
  
  /** If there is a camera bound to this view, copy the coordinates from it. */
  
  public void copyOrientationFromCamera()
  {
    if (boundCamera == null)
      return;
    CoordinateSystem coords = theCamera.getCameraCoordinates();
    coords.copyCoords(boundCamera.coords);
    theCamera.setCameraCoordinates(coords);
  }
  
  /** The following method is called by RotateViewTool when the user has manually moved the
      viewpoint.  This allows the SceneViewer to set the viewpoint choice menu to "Other". */

  public void orientationChanged()
  {
    if (viewChoice != null && viewChoice.getSelectedIndex() < 6)
      viewChoice.setSelectedIndex(viewChoice.getItemCount()-1);
  }
  
  /** Draw a border around the rendered image. */
  
  public void drawBorder()
  {
    Rectangle bounds = getBounds();
    int i, index1, index2;

    if (gr != null)
    {
      gr.setColor(Color.black);
      gr.drawRect(0, 0, bounds.width-1, bounds.height-1);
      if (drawFocus)
        gr.drawRect(1, 1, bounds.width-3, bounds.height-3);
      return;
    }
    index1 = 0;
    index2 = bounds.width-1;
    for (i = 0; i < bounds.height; i++, index1 += bounds.width, index2 += bounds.width)
    {
      pixel[index1] = pixel[index2] = 0xFF000000;
      if (drawFocus)
        pixel[index1+1] = pixel[index2-1] = 0xFF000000;
    }
    index1 = bounds.width*(bounds.height-1);
    for (i = 1; i < bounds.width-1; i++)
    {
      pixel[i] = pixel[index1+i] = 0xFF000000;
      if (drawFocus)
        pixel[i+bounds.width] = pixel[index1+i-bounds.width] = 0xFF000000;
    }
  }

  /** Draw a horizontal line across the rendered image.  The parameters are the y coordinate
      of the line and the line color. */
  
  public void drawHRule(int y, Color color)
  {
    Rectangle bounds = getBounds();
    int i, col, index = y*bounds.width;

    if (gr != null)
      {
        gr.setColor(color);
        gr.drawLine(1, y, bounds.width-1, y);
      }
    else
      {
        col = color.getRGB();
        for (i = 0; i < bounds.width; i++, index++)
          pixel[index] = col;
      }
  }

  /** Draw a vertical line across the rendered image.  The parameters are the x coordinate
      of the line and the line color. */
  
  public void drawVRule(int x, Color color)
  {
    Rectangle bounds = getBounds();
    int i, col, index = x;

    if (gr != null)
      {
        gr.setColor(color);
        gr.drawLine(x, 1, x, bounds.height-1);
      }
    else
      {
        col = color.getRGB();
        for (i = 0; i < bounds.height; i++, index += bounds.width)
          pixel[index] = col;
      }
  }

  /** Draw a filled box in the rendered image. */
  
  public void drawBox(int x, int y, int width, int height, Color color)
  {
    Rectangle bounds = getBounds();
    int i, j, index, maxx, maxy, col;
    
    if (gr != null)
      {
        gr.setColor(color);
        gr.fillRect(x, y, width, height);
      }
    else
      {
        col = color.getRGB();
        maxx = x+width;
        maxy = y+height;
        if (x < 0) x = 0;
        if (y < 0) y = 0;
        if (maxx > bounds.width) maxx = bounds.width;
        if (maxy > bounds.height) maxy = bounds.height;
        width = maxx-x;
        for (i = y, index = y*bounds.width+x; i < maxy; i++, index += bounds.width)
          for (j = 0; j < width; j++)
            pixel[index+j] = col;
      }
  }

  /** Render a filled box at a specified depth in the rendered image. */
  
  public void renderBox(int x, int y, int width, int height, double depth, Color color)
  {
    Rectangle bounds = getBounds();
    int i, j, index, maxx, maxy, col, z;
    
    col = color.getRGB();
    z = (int) (depth*65535.0);
    maxx = x+width;
    maxy = y+height;
    if (x < 0) x = 0;
    if (y < 0) y = 0;
    if (maxx > bounds.width) maxx = bounds.width;
    if (maxy > bounds.height) maxy = bounds.height;
    width = maxx-x;
    for (i = y, index = y*bounds.width+x; i < maxy; i++, index += bounds.width)
      for (j = 0; j < width; j++)
        if (z <= zbuffer[index+j])
          {
            pixel[index+j] = col;
            zbuffer[index+j] = z;
          }
  }

  /** Draw a line into the rendered image. */
  
  public void drawLine(Point p1, Point p2, Color color)
  {
    int x1, y1, x2, y2, col;
    int x, y, dx, dy, delta, end, index, edge;
    
    if (gr != null)
      {
        gr.setColor(color);
        gr.drawLine(p1.x, p1.y, p2.x, p2.y);
        return;
      }
    Rectangle bounds = getBounds();
    col = color.getRGB();
    x1 = p1.x;
    y1 = p1.y;
    x2 = p2.x;
    y2 = p2.y;
    if (x1 < 0 && x2 < 0)
      return;
    if (y1 < 0 && y2 < 0)
      return;
    if (x1 >= bounds.width && x2 >= bounds.width)
      return;
    if (y1 >= bounds.height && y2 >= bounds.height)
      return;
    dx = x2 - x1;
    dy = y2 - y1;
    if (dx == 0 && dy == 0)
      return;
    if (Math.abs(dx) > Math.abs(dy))
      {
        // x is the major axis.
        
        if (dx > 0)
          {
            x = x1;
            y = y1<<16+32768;
            dy = (dy<<16)/dx;
            end = x2 < bounds.width ? x2 : bounds.width;
          }
        else
          {
            x = x2;
            y = y2<<16+32768;
            dy = (dy<<16)/dx;
            end = x1 < bounds.width ? x1 : bounds.width;
          }
        if (x < 0)
          {
            y -= dy*x;
            x = 0;
          }
        edge = bounds.height<<16;
        while (x < end)
          {
            if (y >= 0 && y < edge)
              {
                index = bounds.width*(y>>16)+x;
                pixel[index] = col;
              }
            x++;
            y += dy;
          }
      }
    else
      {
        // y is the major axis.

        if (dy > 0)
          {
            x = x1<<16+32768;
            y = y1;
            dx = (dx<<16)/dy;
            end = y2 < bounds.height ? y2 : bounds.height;
          }
        else
          {
            x = x2<<16+32768;
            y = y2;
            dx = (dx<<16)/dy;
            end = y1 < bounds.height ? y1 : bounds.height;
          }
        if (y < 0)
          {
            x -= dx*y;
            y = 0;
          }
        edge = bounds.width<<16;
        while (y < end)
          {
            if (x >= 0 && x < edge)
              {
                index = y*bounds.width+(x>>16);
                pixel[index] = col;
              }
            x += dx;
            y++;
          }
      }
  }
  
  /** Render a line into the image.
      @param p1     the first endpoint of the line
      @param p2     the second endpoint of the line
      @param cam    the camera from which to draw the line
      @param color  the line color
  */
  
  public void renderLine(Vec3 p1, Vec3 p2, Camera cam, Color color)
  {
    if (cam.isPerspective())
    {
      double z1 = cam.getObjectToView().timesZ(p1);
      double z2 = cam.getObjectToView().timesZ(p2);
      double clip = cam.getClipDistance();
      if (z1 < clip)
      {
        if (z2 < clip)
          return;
        double f = ((double) (clip-z1))/(z2-z1);
        p1 = new Vec3(p1.x+f*(p2.x-p1.x), p1.y+f*(p2.y-p1.y), p1.z+f*(p2.z-p1.z));
      }
      else if (z2 < clip)
      {
        double f = ((double) (clip-z2))/(z1-z2);
        p2 = new Vec3(p2.x+f*(p1.x-p2.x), p2.y+f*(p1.y-p2.y), p2.z+f*(p1.z-p2.z));
      }
    }
    renderLine(cam.getObjectToScreen().timesXY(p1), cam.getObjectToView().timesZ(p1),
        cam.getObjectToScreen().timesXY(p2), cam.getObjectToView().timesZ(p2),
        cam, color);
  }

  /** Render a line into the image.
      @param p1     the first endpoint of the line, in screen coordinates
      @param zf1    the z coordinate of the first endpoint, in view coordinates
      @param p2     the second endpoint of the line, in screen coordinates
      @param zf2    the z coordinate of the second endpoint, in view coordinates
      @param cam    the camera from which to draw the line
      @param color  the line color
  */
  
  public void renderLine(Vec2 p1, double zf1, Vec2 p2, double zf2, Camera cam, Color color)
  {
    int x1, y1, z1, x2, y2, z2;
    int x, y, z, dx, dy, dz, delta, end, index, edge;
    int clip = (int) (cam.isPerspective() ? cam.getClipDistance()*65535.0 : Integer.MIN_VALUE);
    int rgb = color.getRGB();
    Rectangle bounds = getBounds();
    
    x1 = (int) p1.x;
    y1 = (int) p1.y;
    z1 = (int) (zf1*65535.0);
    x2 = (int) p2.x;
    y2 = (int) p2.y;
    z2 = (int) (zf2*65535.0);
    if (x1 < 0 && x2 < 0)
      return;
    if (y1 < 0 && y2 < 0)
      return;
    if (x1 >= bounds.width && x2 >= bounds.width)
      return;
    if (y1 >= bounds.height && y2 >= bounds.height)
      return;
    if (z1 < clip && z2 < clip)
      return;
    if (renderMode == RENDER_WIREFRAME)
    {
      drawLine(new Point(x1, y1), new Point(x2, y2), color);
      return;
    }
    dx = x2 - x1;
    dy = y2 - y1;
    dz = z2 - z1;
    if (dx == 0 && dy == 0)
      return;
    if (Math.abs(dx) > Math.abs(dy))
      {
        // x is the major axis.
        
        if (dx > 0)
          {
            x = x1;
            y = y1<<16+32768;
            z = z1;
            dy = (dy<<16)/dx;
            dz = dz/dx;
            end = x2 < bounds.width ? x2 : bounds.width;
          }
        else
          {
            x = x2;
            y = y2<<16+32768;
            z = z2;
            dy = (dy<<16)/dx;
            dz = dz/dx;
            end = x1 < bounds.width ? x1 : bounds.width;
          }
        if (x < 0)
          {
            y -= dy*x;
            z -= dz*x;
            x = 0;
          }
        edge = bounds.height<<16;
        while (x < end)
          {
            if (y >= 0 && y < edge && z > clip)
              {
                index = bounds.width*(y>>16)+x;
                if (z <= zbuffer[index])
                  {
                    pixel[index] = rgb;
                    zbuffer[index] = z;
                  }
              }
            x++;
            y += dy;
            z += dz;
          }
      }
    else
      {
        // y is the major axis.

        if (dy > 0)
          {
            x = x1<<16+32768;
            y = y1;
            z = z1;
            dx = (dx<<16)/dy;
            dz = dz/dy;
            end = y2 < bounds.height ? y2 : bounds.height;
          }
        else
          {
            x = x2<<16+32768;
            y = y2;
            z = z2;
            dx = (dx<<16)/dy;
            dz = dz/dy;
            end = y1 < bounds.height ? y1 : bounds.height;
          }
        if (y < 0)
          {
            x -= dx*y;
            z -= dz*y;
            y = 0;
          }
        edge = bounds.width<<16;
        while (y < end)
          {
            if (x >= 0 && x < edge && z > clip)
              {
                index = y*bounds.width+(x>>16);
                if (z <= zbuffer[index])
                  {
                    pixel[index] = rgb;
                    zbuffer[index] = z;
                  }
              }
            x += dx;
            y++;
            z += dz;
          }
      }
  }
  
  /** Render a wireframe object. */
  
  public void renderWireframe(WireframeMesh mesh, Camera cam)
  {
    Vec3 vert[] = mesh.vert;
    int from[] = mesh.from;
    int to[] = mesh.to;
    for (int i = 0; i < from.length; i++)
      renderLine(vert[from[i]], vert[to[i]], cam, Color.black);
  }
  
  /** Clip a triangle to the region in front of the z clipping plane. */
  
  private Vec2 [] clipTriangle(Vec3 v1, Vec3 v2, Vec3 v3, double z1, double z2, double z3, Camera cam, double newz[])
  {
    double clip = cam.getClipDistance();
    Mat4 toScreen = cam.getObjectToScreen();
    boolean c1 = z1 < clip, c2 = z2 < clip, c3 = z3 < clip;
    Vec3 u1, u2, u3, u4;
    int clipCount = 0;
    
    if (c1) clipCount++;
    if (c2) clipCount++;
    if (c3) clipCount++;
    if (clipCount == 2)
      {
        // Two vertices need to be clipped.
        
        if (!c1)
          {
            u1 = v1;
            newz[0] = z1;
            double f2 = (z1-clip)/(z1-z2), f1 = 1.0-f2;
            u2 = new Vec3(f1*v1.x+f2*v2.x, f1*v1.y+f2*v2.y, f1*v1.z+f2*v2.z);
            newz[1] = f1*z1 + f2*z2;
            f2 = (z1-clip)/(z1-z3);
            f1 = 1.0-f2;
            u3 = new Vec3(f1*v1.x+f2*v3.x, f1*v1.y+f2*v3.y, f1*v1.z+f2*v3.z);
            newz[2] = f1*z1 + f2*z3;
          }
        else if (!c2)
          {
            u2 = v2;
            newz[1] = z2;
            double f2 = (z2-clip)/(z2-z3), f1 = 1.0-f2;
            u3 = new Vec3(f1*v2.x+f2*v3.x, f1*v2.y+f2*v3.y, f1*v2.z+f2*v3.z);
            newz[2] = f1*z2 + f2*z3;
            f2 = (z2-clip)/(z2-z1);
            f1 = 1.0-f2;
            u1 = new Vec3(f1*v2.x+f2*v1.x, f1*v2.y+f2*v1.y, f1*v2.z+f2*v1.z);
            newz[0] = f1*z2 + f2*z1;
          }
        else
          {
            u3 = v3;
            newz[2] = z3;
            double f2 = (z3-clip)/(z3-z1), f1 = 1.0-f2;
            u1 = new Vec3(f1*v3.x+f2*v1.x, f1*v3.y+f2*v1.y, f1*v3.z+f2*v1.z);
            newz[0] = f1*z3 + f2*z1;
            f2 = (z3-clip)/(z3-z2);
            f1 = 1.0-f2;
            u2 = new Vec3(f1*v3.x+f2*v2.x, f1*v3.y+f2*v2.y, f1*v3.z+f2*v2.z);
            newz[1] = f1*z3 + f2*z2;
          }
        return new Vec2 [] {toScreen.timesXY(u1), toScreen.timesXY(u2), toScreen.timesXY(u3)};
      }
    
    // Only one vertex needs to be clipped, resulting in a quad.
    
    if (c1)
      {
        u1 = v2;
        newz[0] = z2;
        u2 = v3;
        newz[1] = z3;
        double f1 = (z2-clip)/(z2-z1), f2 = 1.0-f1;
        u3 = new Vec3(f1*v1.x+f2*v2.x, f1*v1.y+f2*v2.y, f1*v1.z+f2*v2.z);
        newz[2] = f1*z1 + f2*z2;
        f1 = (z3-clip)/(z3-z1);
        f2 = 1.0-f1;
        u4 = new Vec3(f1*v1.x+f2*v3.x, f1*v1.y+f2*v3.y, f1*v1.z+f2*v3.z);
        newz[3] = f1*z1 + f2*z3;
      }
    else if (c2)
      {
        u1 = v3;
        newz[0] = z3;
        u2 = v1;
        newz[1] = z1;
        double f1 = (z3-clip)/(z3-z2), f2 = 1.0-f1;
        u3 = new Vec3(f1*v2.x+f2*v3.x, f1*v2.y+f2*v3.y, f1*v2.z+f2*v3.z);
        newz[2] = f1*z2 + f2*z3;
        f1 = (z1-clip)/(z1-z2);
        f2 = 1.0-f1;
        u4 = new Vec3(f1*v2.x+f2*v1.x, f1*v2.y+f2*v1.y, f1*v2.z+f2*v1.z);
        newz[3] = f1*z2 + f2*z1;
      }
    else
      {
        u1 = v1;
        newz[0] = z1;
        u2 = v2;
        newz[1] = z2;
        double f1 = (z1-clip)/(z1-z3), f2 = 1.0-f1;
        u3 = new Vec3(f1*v3.x+f2*v1.x, f1*v3.y+f2*v1.y, f1*v3.z+f2*v1.z);
        newz[2] = f1*z3 + f2*z1;
        f1 = (z2-clip)/(z2-z3);
        f2 = 1.0-f1;
        u4 = new Vec3(f1*v3.x+f2*v2.x, f1*v3.y+f2*v2.y, f1*v3.z+f2*v2.z);
        newz[3] = f1*z3 + f2*z2;
      }
    return new Vec2 [] {toScreen.timesXY(u1), toScreen.timesXY(u2), toScreen.timesXY(u3), toScreen.timesXY(u4)};
  }

  /** Render an object with flat shading in subtractive (transparent) mode. */
  
  public void renderMeshTransparent(RenderingMesh mesh, VertexShader shader, Camera cam, Vec3 viewDir, boolean hideFace[])
  {
    Vec3 vert[] = mesh.vert;
    Vec2 pos[] = new Vec2 [vert.length];
    double z[] = new double [vert.length], clip = (cam.isPerspective() ? cam.getClipDistance() : -Double.MAX_VALUE);
    double clipz[] = new double [4];
    Mat4 toView = cam.getObjectToView(), toScreen = cam.getObjectToScreen();
    Rectangle bounds = getBounds();
    float dot;
    RGBColor faceColor = new RGBColor(0.0f, 0.0f, 0.0f);
    RenderingTriangle tri;
    int i, v1, v2, v3;
    
    int numToReuse = Math.min(vert.length, reuseVec2.length);
    for (i = 0; i < numToReuse; i++)
      {
        pos[i] = toScreen.timesXY(vert[i], reuseVec2[i]);
        z[i] = toView.timesZ(vert[i]);
      }
    for (i = numToReuse; i < vert.length; i++)
      {
        pos[i] = toScreen.timesXY(vert[i]);
        z[i] = toView.timesZ(vert[i]);
      }
    for (i = 0; i < mesh.triangle.length; i++)
      {
        if (hideFace != null && hideFace[i])
          continue;
        tri = mesh.triangle[i];
        v1 = tri.v1;
        v2 = tri.v2;
        v3 = tri.v3;
        if (z[v1] < clip && z[v2] < clip && z[v3] < clip)
          continue;
        dot = (float) viewDir.dot(mesh.faceNorm[i]);
        shader.getColor(i, 0, faceColor);
        faceColor.setRGB(1.0f-faceColor.getRed(), 1.0f-faceColor.getGreen(), 1.0f-faceColor.getBlue());
        faceColor.scale(1.0f-0.8f*Math.abs(dot));
        if (z[v1] < clip || z[v2] < clip || z[v3] < clip)
          {
            Vec2 clipPos[] = clipTriangle(vert[v1], vert[v2], vert[v3], z[v1], z[v2], z[v3], cam, clipz);
            boolean inside = true;
            for (int j = 0; j < clipPos.length && true; j++)
              if (clipPos[j].x < -32767.0 || clipPos[j].x > 32767.0 || clipPos[j].y < -32767.0 || clipPos[j].y > 32767.0)
                inside = false;
            if (!inside)
              continue;
            for (int j = 0; j < clipPos.length-2; j++)
              renderFlatTriangle(clipPos[j], clipz[j], clipPos[j+1], clipz[j+1], clipPos[j+2], clipz[j+2], bounds.width, bounds.height, clip, true, faceColor);
          }
        else
          renderFlatTriangle(pos[v1], z[v1], pos[v2], z[v2], pos[v3], z[v3], bounds.width, bounds.height, clip, true, faceColor);
      }
  }
  
  /** Render a mesh to the canvas. */
  
  public void renderMesh(RenderingMesh mesh, VertexShader shader, Camera cam, boolean closed, boolean hideFace[])
  {
    Vec3 vert[] = mesh.vert, norm[] = mesh.norm;
    Vec2 pos[] = new Vec2 [vert.length];
    double z[] = new double [vert.length], clip = (cam.isPerspective() ? cam.getClipDistance() : -Double.MAX_VALUE), dot;
    double clipz[] = new double [4];
    Mat4 toView = cam.getObjectToView(), toScreen = cam.getObjectToScreen();
    Rectangle bounds = getBounds();
    RGBColor color1 = new RGBColor(), color2 = new RGBColor(), color3 = new RGBColor();
    RGBColor color4 = new RGBColor(), color5 = new RGBColor(), color6 = new RGBColor(), color7 = new RGBColor();
    RenderingTriangle tri;
    int i, v1, v2, v3, n1, n2, n3;
    boolean backface, needClipping;

    if (hideFace != null)
      closed = false;
    int numToReuse = Math.min(vert.length, reuseVec2.length);
    for (i = 0; i < numToReuse; i++)
      {
        pos[i] = toScreen.timesXY(vert[i], reuseVec2[i]);
        z[i] = toView.timesZ(vert[i]);
      }
    for (i = numToReuse; i < vert.length; i++)
      {
        pos[i] = toScreen.timesXY(vert[i]);
        z[i] = toView.timesZ(vert[i]);
      }
    for (i = 0; i < mesh.triangle.length; i++)
      {
        if (hideFace != null && hideFace[i])
          continue;
        tri = mesh.triangle[i];
        v1 = tri.v1;
        v2 = tri.v2;
        v3 = tri.v3;
        if (z[v1] < clip && z[v2] < clip && z[v3] < clip)
          continue;
        backface = ((closed && hideBackfaces) && ((pos[v2].x-pos[v1].x)*(pos[v3].y-pos[v1].y) - (pos[v2].y-pos[v1].y)*(pos[v3].x-pos[v1].x) > 0.0));
        needClipping = (z[v1] < clip || z[v2] < clip || z[v3] < clip);
        if (backface && !needClipping)
          continue;
        shader.getColor(i, 0, color1);
        if (shader.isUniformFace(i))
          {
            // This triangle is solid colored, so use the faster rendering method.
            
            if (needClipping)
              {
                Vec2 clipPos[] = clipTriangle(vert[v1], vert[v2], vert[v3], z[v1], z[v2], z[v3], cam, clipz);
                boolean inside = true;
                for (int j = 0; j < clipPos.length && true; j++)
                  if (clipPos[j].x < -32767.0 || clipPos[j].x > 32767.0 || clipPos[j].y < -32767.0 || clipPos[j].y > 32767.0)
                    inside = false;
                if (!inside)
                  continue;
                for (int j = 0; j < clipPos.length-2; j++)
                  renderFlatTriangle(clipPos[j], clipz[j], clipPos[j+1], clipz[j+1], clipPos[j+2], clipz[j+2], bounds.width, bounds.height, clip, false, color1);
              }
            else
              renderFlatTriangle(pos[v1], z[v1], pos[v2], z[v2], pos[v3], z[v3], bounds.width, bounds.height, clip, false, color1);
            continue;
          }
        shader.getColor(i, 1, color2);
        shader.getColor(i, 2, color3);
        if (needClipping)
          {
            Vec2 clipPos[] = clipSmoothTriangle(vert[v1], vert[v2], vert[v3], z[v1], z[v2], z[v3], cam, color1, color2, color3, color4, color5, color6, color7, clipz);
            boolean inside = true;
            for (int j = 0; j < clipPos.length && true; j++)
              if (clipPos[j].x < -32767.0 || clipPos[j].x > 32767.0 || clipPos[j].y < -32767.0 || clipPos[j].y > 32767.0)
                inside = false;
            if (!inside)
              continue;
            renderSmoothTriangle(clipPos[0], clipz[0], clipPos[1], clipz[1], clipPos[2], clipz[2], bounds.width, bounds.height, clip, color4, color5, color6);
            if (clipPos.length == 4)
              renderSmoothTriangle(clipPos[1], clipz[1], clipPos[2], clipz[2], clipPos[3], clipz[3], bounds.width, bounds.height, clip, color5, color6, color7);
          }
        else
          renderSmoothTriangle(pos[v1], z[v1], pos[v2], z[v2], pos[v3], z[v3], bounds.width, bounds.height, clip, color1, color2, color3);
      }
  }

  /** Clip a smooth shaded triangle to the region in front of the z clipping plane. */
  
  private Vec2 [] clipSmoothTriangle(Vec3 v1, Vec3 v2, Vec3 v3, double z1, double z2, double z3, Camera cam, RGBColor col1, RGBColor col2, RGBColor col3, RGBColor newc1, RGBColor newc2, RGBColor newc3, RGBColor newc4, double newz[])
  {
    double clip = cam.getClipDistance();
    Mat4 toScreen = cam.getObjectToScreen();
    boolean c1 = z1 < clip, c2 = z2 < clip, c3 = z3 < clip;
    Vec3 u1, u2, u3, u4;
    int clipCount = 0;
    
    if (c1) clipCount++;
    if (c2) clipCount++;
    if (c3) clipCount++;
    if (clipCount == 2)
      {
        // Two vertices need to be clipped.
        
        if (!c1)
          {
            u1 = v1;
            newz[0] = z1;
            newc1.copy(col1);
            double f2 = (z1-clip)/(z1-z2), f1 = 1.0-f2;
            u2 = new Vec3(f1*v1.x+f2*v2.x, f1*v1.y+f2*v2.y, f1*v1.z+f2*v2.z);
            newc2.setRGB(f1*col1.getRed()+f2*col2.getRed(), f1*col1.getGreen()+f2*col2.getGreen(), f1*col1.getBlue()+f2*col2.getBlue());
            newz[1] = f1*z1 + f2*z2;
            f2 = (z1-clip)/(z1-z3);
            f1 = 1.0-f2;
            u3 = new Vec3(f1*v1.x+f2*v3.x, f1*v1.y+f2*v3.y, f1*v1.z+f2*v3.z);
            newc3.setRGB(f1*col1.getRed()+f2*col3.getRed(), f1*col1.getGreen()+f2*col3.getGreen(), f1*col1.getBlue()+f2*col3.getBlue());
            newz[2] = f1*z1 + f2*z3;
          }
        else if (!c2)
          {
            u2 = v2;
            newz[1] = z2;
            newc2.copy(col2);
            double f2 = (z2-clip)/(z2-z3), f1 = 1.0-f2;
            u3 = new Vec3(f1*v2.x+f2*v3.x, f1*v2.y+f2*v3.y, f1*v2.z+f2*v3.z);
            newc3.setRGB(f1*col2.getRed()+f2*col3.getRed(), f1*col2.getGreen()+f2*col3.getGreen(), f1*col2.getBlue()+f2*col3.getBlue());
            newz[2] = f1*z2 + f2*z3;
            f2 = (z2-clip)/(z2-z1);
            f1 = 1.0-f2;
            u1 = new Vec3(f1*v2.x+f2*v1.x, f1*v2.y+f2*v1.y, f1*v2.z+f2*v1.z);
            newc1.setRGB(f1*col2.getRed()+f2*col3.getRed(), f1*col2.getGreen()+f2*col3.getGreen(), f1*col2.getBlue()+f2*col3.getBlue());
            newz[0] = f1*z2 + f2*z1;
          }
        else
          {
            u3 = v3;
            newz[2] = z3;
            newc3.copy(col3);
            double f2 = (z3-clip)/(z3-z1), f1 = 1.0-f2;
            u1 = new Vec3(f1*v3.x+f2*v1.x, f1*v3.y+f2*v1.y, f1*v3.z+f2*v1.z);
            newc1.setRGB(f1*col3.getRed()+f2*col1.getRed(), f1*col3.getGreen()+f2*col1.getGreen(), f1*col3.getBlue()+f2*col1.getBlue());
            newz[0] = f1*z3 + f2*z1;
            f2 = (z3-clip)/(z3-z2);
            f1 = 1.0-f2;
            u2 = new Vec3(f1*v3.x+f2*v2.x, f1*v3.y+f2*v2.y, f1*v3.z+f2*v2.z);
            newc2.setRGB(f1*col3.getRed()+f2*col2.getRed(), f1*col3.getGreen()+f2*col2.getGreen(), f1*col3.getBlue()+f2*col2.getBlue());
            newz[1] = f1*z3 + f2*z2;
          }
        return new Vec2 [] {toScreen.timesXY(u1), toScreen.timesXY(u2), toScreen.timesXY(u3)};
      }
    
    // Only one vertex needs to be clipped, resulting in a quad.
    
    if (c1)
      {
        u1 = v2;
        newz[0] = z2;
        newc1.copy(col2);
        u2 = v3;
        newz[1] = z3;
        newc2.copy(col3);
        double f1 = (z2-clip)/(z2-z1), f2 = 1.0-f1;
        u3 = new Vec3(f1*v1.x+f2*v2.x, f1*v1.y+f2*v2.y, f1*v1.z+f2*v2.z);
        newc3.setRGB(f1*col1.getRed()+f2*col2.getRed(), f1*col1.getGreen()+f2*col2.getGreen(), f1*col1.getBlue()+f2*col2.getBlue());
        newz[2] = f1*z1 + f2*z2;
        f1 = (z3-clip)/(z3-z1);
        f2 = 1.0-f1;
        u4 = new Vec3(f1*v1.x+f2*v3.x, f1*v1.y+f2*v3.y, f1*v1.z+f2*v3.z);
        newc4.setRGB(f1*col1.getRed()+f2*col3.getRed(), f1*col1.getGreen()+f2*col3.getGreen(), f1*col1.getBlue()+f2*col3.getBlue());
        newz[3] = f1*z1 + f2*z3;
      }
    else if (c2)
      {
        u1 = v3;
        newz[0] = z3;
        newc1.copy(col3);
        u2 = v1;
        newz[1] = z1;
        newc2.copy(col1);
        double f1 = (z3-clip)/(z3-z2), f2 = 1.0-f1;
        u3 = new Vec3(f1*v2.x+f2*v3.x, f1*v2.y+f2*v3.y, f1*v2.z+f2*v3.z);
        newc3.setRGB(f1*col2.getRed()+f2*col3.getRed(), f1*col2.getGreen()+f2*col3.getGreen(), f1*col2.getBlue()+f2*col3.getBlue());
        newz[2] = f1*z2 + f2*z3;
        f1 = (z1-clip)/(z1-z2);
        f2 = 1.0-f1;
        u4 = new Vec3(f1*v2.x+f2*v1.x, f1*v2.y+f2*v1.y, f1*v2.z+f2*v1.z);
        newc4.setRGB(f1*col2.getRed()+f2*col1.getRed(), f1*col2.getGreen()+f2*col1.getGreen(), f1*col2.getBlue()+f2*col1.getBlue());
        newz[3] = f1*z2 + f2*z1;
      }
    else
      {
        u1 = v1;
        newz[0] = z1;
        newc1.copy(col1);
        u2 = v2;
        newz[1] = z2;
        newc2.copy(col2);
        double f1 = (z1-clip)/(z1-z3), f2 = 1.0-f1;
        u3 = new Vec3(f1*v3.x+f2*v1.x, f1*v3.y+f2*v1.y, f1*v3.z+f2*v1.z);
        newc3.setRGB(f1*col3.getRed()+f2*col1.getRed(), f1*col3.getGreen()+f2*col1.getGreen(), f1*col3.getBlue()+f2*col1.getBlue());
        newz[2] = f1*z3 + f2*z1;
        f1 = (z2-clip)/(z2-z3);
        f2 = 1.0-f1;
        u4 = new Vec3(f1*v3.x+f2*v2.x, f1*v3.y+f2*v2.y, f1*v3.z+f2*v2.z);
        newc4.setRGB(f1*col3.getRed()+f2*col2.getRed(), f1*col3.getGreen()+f2*col2.getGreen(), f1*col3.getBlue()+f2*col2.getBlue());
        newz[3] = f1*z3 + f2*z2;
      }
    return new Vec2 [] {toScreen.timesXY(u1), toScreen.timesXY(u2), toScreen.timesXY(u3), toScreen.timesXY(u4)};
  }
  
  /** Render a solid colored triangle. */

  public void renderFlatTriangle(Vec2 pos1, double zf1, Vec2 pos2, double zf2, Vec2 pos3, double zf3, int width, int height, double clip, boolean transparent, RGBColor color)
  {
    int x1, y1, z1, x2, y2, z2, x3, y3, z3;
    int dx1, dx2, dy1, dy2, dz1, dz2, mx1, mx2, mz1, mz2;
    int xstart, xend, yend, zstart, zend, y, z, dz, left, right, i, index;
    int clipDist = (int) (clip*65535.0), r, g, b, red, green, blue, col;
    
    if (transparent)
      {
        col = 0;
        red = (int) (color.getRed()*255.0f);
        green = (int) (color.getGreen()*255.0f);
        blue = (int) (color.getBlue()*255.0f);
      }
    else
      {
        col = color.getARGB();
        red = green = blue = 0;
      }
    if (pos1.y <= pos2.y && pos1.y <= pos3.y)
      {
        x1 = ((int) pos1.x) << 16;
        y1 = ((int) pos1.y);
        z1 = (int) (zf1*65535.0);
        if (pos2.y < pos3.y)
          {
            x2 = ((int) pos2.x) << 16;
            y2 = ((int) pos2.y);
            z2 = (int) (zf2*65535.0);
            x3 = ((int) pos3.x) << 16;
            y3 = ((int) pos3.y);
            z3 = (int) (zf3*65535.0);
          }
        else
          {
            x2 = ((int) pos3.x) << 16;
            y2 = ((int) pos3.y);
            z2 = (int) (zf3*65535.0);
            x3 = ((int) pos2.x) << 16;
            y3 = ((int) pos2.y);
            z3 = (int) (zf2*65535.0);
          }
      }
    else if (pos2.y <= pos1.y && pos2.y <= pos3.y)
      {
        x1 = ((int) pos2.x) << 16;
        y1 = ((int) pos2.y);
        z1 = (int) (zf2*65535.0);
        if (pos1.y < pos3.y)
          {
            x2 = ((int) pos1.x) << 16;
            y2 = ((int) pos1.y);
            z2 = (int) (zf1*65535.0);
            x3 = ((int) pos3.x) << 16;
            y3 = ((int) pos3.y);
            z3 = (int) (zf3*65535.0);
          }
        else
          {
            x2 = ((int) pos3.x) << 16;
            y2 = ((int) pos3.y);
            z2 = (int) (zf3*65535.0);
            x3 = ((int) pos1.x) << 16;
            y3 = ((int) pos1.y);
            z3 = (int) (zf1*65535.0);
          }
      }
    else
      {
        x1 = ((int) pos3.x) << 16;
        y1 = ((int) pos3.y);
        z1 = (int) (zf3*65535.0);
        if (pos1.y < pos2.y)
          {
            x2 = ((int) pos1.x) << 16;
            y2 = ((int) pos1.y);
            z2 = (int) (zf1*65535.0);
            x3 = ((int) pos2.x) << 16;
            y3 = ((int) pos2.y);
            z3 = (int) (zf2*65535.0);
          }
        else
          {
            x2 = ((int) pos2.x) << 16;
            y2 = ((int) pos2.y);
            z2 = (int) (zf2*65535.0);
            x3 = ((int) pos1.x) << 16;
            y3 = ((int) pos1.y);
            z3 = (int) (zf1*65535.0);
          }
      }
    dx1 = x3-x1;
    dy1 = y3-y1;
    dz1 = z3-z1;
    if (dy1 == 0)
      return;
    dx2 = x2-x1;
    dy2 = y2-y1;
    dz2 = z2-z1;
    mx1 = dx1/dy1;
    mz1 = dz1/dy1;
    xstart = xend = x1;
    zstart = zend = z1;
    y = y1;
    if (dy2 != 0)
      {
        mx2 = dx2/dy2;
        mz2 = dz2/dy2;
        if (y2 < 0)
          {
            xstart += mx1*dy2;
            xend += mx2*dy2;
            zstart += mz1*dy2;
            zend += mz2*dy2;
            y = y2;
          }
        else if (y < 0)
          {
            xstart -= mx1*y;
            xend -= mx2*y;
            zstart -= mz1*y;
            zend -= mz2*y;
            y = 0;
          }
        yend = (y2 < height ? y2 : height);
        index = y*width;
        while (y < yend)
          {
            if (xstart < xend)
              {
                left = xstart >> 16;
                right = xend >> 16;
                z = zstart;
                dz = zend-zstart;
              }
            else
              {
                left = xend >> 16;
                right = xstart >> 16;
                z = zend;
                dz = zstart-zend;
              }
            if (left != right)
              {
                dz /= (right-left);
                if (left < 0)
                  {
                    z -= left*dz;
                    left = 0;
                  }
                if (right > width)
                  right = width;
                if (transparent)
                  for (i = left; i < right; i++)
                    {
                      if (z > clipDist)
                        {
                          r = ((pixel[index+i] & 0xFF0000) >> 16) - red;
                          g = ((pixel[index+i] & 0xFF00) >> 8) - green;
                          b = (pixel[index+i] & 0xFF) - blue;
                          if (r < 0) r = 0;
                          if (g < 0) g = 0;
                          if (b < 0) b = 0;
                          pixel[index+i] = 0xFF000000 + (r<<16) + (g<<8) + b;
                        }
                      z += dz;
                    }
                else
                  for (i = left; i < right; i++)
                    {
                      if (z < zbuffer[index+i] && z > clipDist)
                        {
                          pixel[index+i] = col;
                          zbuffer[index+i] = z;
                        }
                      z += dz;
                    }
              }
            xstart += mx1;
            zstart += mz1;
            xend += mx2;
            zend += mz2;
            index += width;
            y++;
          }
      }
    dx2 = x3-x2;
    dy2 = y3-y2;
    dz2 = z3-z2;
    if (dy2 != 0)
      {
        mx2 = dx2/dy2;
        mz2 = dz2/dy2;
        xend = x2;
        zend = z2;
        if (y < 0)
          {
            xstart -= mx1*y;
            xend -= mx2*y;
            zstart -= mz1*y;
            zend -= mz2*y;
            y = 0;
          }
        yend = (y3 < height ? y3 : height);
        index = y*width;
        while (y < yend)
          {
            if (xstart < xend)
              {
                left = xstart >> 16;
                right = xend >> 16;
                z = zstart;
                dz = zend-zstart;
              }
            else
              {
                left = xend >> 16;
                right = xstart >> 16;
                z = zend;
                dz = zstart-zend;
              }
            if (left != right)
              {
                dz /= (right-left);
                if (left < 0)
                  {
                    z -= left*dz;
                    left = 0;
                  }
                if (right > width)
                  right = width;
                if (transparent)
                  for (i = left; i < right; i++)
                    {
                      if (z > clipDist)
                        {
                          r = ((pixel[index+i] & 0xFF0000) >> 16) - red;
                          g = ((pixel[index+i] & 0xFF00) >> 8) - green;
                          b = (pixel[index+i] & 0xFF) - blue;
                          if (r < 0) r = 0;
                          if (g < 0) g = 0;
                          if (b < 0) b = 0;
                          pixel[index+i] = 0xFF000000 + (r<<16) + (g<<8) + b;
                        }
                      z += dz;
                    }
                else
                  for (i = left; i < right; i++)
                    {
                      if (z < zbuffer[index+i] && z > clipDist)
                        {
                          pixel[index+i] = col;
                          zbuffer[index+i] = z;
                        }
                      z += dz;
                    }
              }
            xstart += mx1;
            zstart += mz1;
            xend += mx2;
            zend += mz2;
            index += width;
            y++;
          }
      }
  }

  /** Render a triangle with smooth (interpolated) shading. */

  public void renderSmoothTriangle(Vec2 pos1, double zf1, Vec2 pos2, double zf2, Vec2 pos3, double zf3, int width, int height, double clip, RGBColor color1, RGBColor color2, RGBColor color3)
  {
    int x1, y1, z1, x2, y2, z2, x3, y3, z3;
    int dx1, dx2, dy1, dy2, dz1, dz2, mx1, mx2, mz1, mz2;
    int xstart, xend, yend, zstart, zend, y, z, dz, left, right, i, index;
    int red1, green1, blue1, red2, green2, blue2, red3, green3, blue3;
    int dred1, dred2, dgreen1, dgreen2, dblue1, dblue2, mred1, mred2, mgreen1, mgreen2, mblue1, mblue2;
    int redstart, redend, greenstart, greenend, bluestart, blueend, red, green, blue, dred, dgreen, dblue;
    int clipDist = (int) (clip*65535.0);
    
    if (pos1.y <= pos2.y && pos1.y <= pos3.y)
      {
        x1 = ((int) pos1.x) << 16;
        y1 = ((int) pos1.y);
        z1 = (int) (zf1*65535.0);
        red1 = (int) (color1.getRed()*65535.0f);
        green1 = (int) (color1.getGreen()*65535.0f);
        blue1 = (int) (color1.getBlue()*65535.0f);
        if (pos2.y < pos3.y)
          {
            x2 = ((int) pos2.x) << 16;
            y2 = ((int) pos2.y);
            z2 = (int) (zf2*65535.0);
            red2 = (int) (color2.getRed()*65535.0f);
            green2 = (int) (color2.getGreen()*65535.0f);
            blue2 = (int) (color2.getBlue()*65535.0f);
            x3 = ((int) pos3.x) << 16;
            y3 = ((int) pos3.y);
            z3 = (int) (zf3*65535.0);
            red3 = (int) (color3.getRed()*65535.0f);
            green3 = (int) (color3.getGreen()*65535.0f);
            blue3 = (int) (color3.getBlue()*65535.0f);
          }
        else
          {
            x2 = ((int) pos3.x) << 16;
            y2 = ((int) pos3.y);
            z2 = (int) (zf3*65535.0);
            red2 = (int) (color3.getRed()*65535.0f);
            green2 = (int) (color3.getGreen()*65535.0f);
            blue2 = (int) (color3.getBlue()*65535.0f);
            x3 = ((int) pos2.x) << 16;
            y3 = ((int) pos2.y);
            z3 = (int) (zf2*65535.0);
            red3 = (int) (color2.getRed()*65535.0f);
            green3 = (int) (color2.getGreen()*65535.0f);
            blue3 = (int) (color2.getBlue()*65535.0f);
          }
      }
    else if (pos2.y <= pos1.y && pos2.y <= pos3.y)
      {
        x1 = ((int) pos2.x) << 16;
        y1 = ((int) pos2.y);
        z1 = (int) (zf2*65535.0);
        red1 = (int) (color2.getRed()*65535.0f);
        green1 = (int) (color2.getGreen()*65535.0f);
        blue1 = (int) (color2.getBlue()*65535.0f);
        if (pos1.y < pos3.y)
          {
            x2 = ((int) pos1.x) << 16;
            y2 = ((int) pos1.y);
            z2 = (int) (zf1*65535.0);
            red2 = (int) (color1.getRed()*65535.0f);
            green2 = (int) (color1.getGreen()*65535.0f);
            blue2 = (int) (color1.getBlue()*65535.0f);
            x3 = ((int) pos3.x) << 16;
            y3 = ((int) pos3.y);
            z3 = (int) (zf3*65535.0);
            red3 = (int) (color3.getRed()*65535.0f);
            green3 = (int) (color3.getGreen()*65535.0f);
            blue3 = (int) (color3.getBlue()*65535.0f);
          }
        else
          {
            x2 = ((int) pos3.x) << 16;
            y2 = ((int) pos3.y);
            z2 = (int) (zf3*65535.0);
            red2 = (int) (color3.getRed()*65535.0f);
            green2 = (int) (color3.getGreen()*65535.0f);
            blue2 = (int) (color3.getBlue()*65535.0f);
            x3 = ((int) pos1.x) << 16;
            y3 = ((int) pos1.y);
            z3 = (int) (zf1*65535.0);
            red3 = (int) (color1.getRed()*65535.0f);
            green3 = (int) (color1.getGreen()*65535.0f);
            blue3 = (int) (color1.getBlue()*65535.0f);
          }
      }
    else
      {
        x1 = ((int) pos3.x) << 16;
        y1 = ((int) pos3.y);
        z1 = (int) (zf3*65535.0);
        red1 = (int) (color3.getRed()*65535.0f);
        green1 = (int) (color3.getGreen()*65535.0f);
        blue1 = (int) (color3.getBlue()*65535.0f);
        if (pos1.y < pos2.y)
          {
            x2 = ((int) pos1.x) << 16;
            y2 = ((int) pos1.y);
            z2 = (int) (zf1*65535.0);
            red2 = (int) (color1.getRed()*65535.0f);
            green2 = (int) (color1.getGreen()*65535.0f);
            blue2 = (int) (color1.getBlue()*65535.0f);
            x3 = ((int) pos2.x) << 16;
            y3 = ((int) pos2.y);
            z3 = (int) (zf2*65535.0);
            red3 = (int) (color2.getRed()*65535.0f);
            green3 = (int) (color2.getGreen()*65535.0f);
            blue3 = (int) (color2.getBlue()*65535.0f);
          }
        else
          {
            x2 = ((int) pos2.x) << 16;
            y2 = ((int) pos2.y);
            z2 = (int) (zf2*65535.0);
            red2 = (int) (color2.getRed()*65535.0f);
            green2 = (int) (color2.getGreen()*65535.0f);
            blue2 = (int) (color2.getBlue()*65535.0f);
            x3 = ((int) pos1.x) << 16;
            y3 = ((int) pos1.y);
            z3 = (int) (zf1*65535.0);
            red3 = (int) (color1.getRed()*65535.0f);
            green3 = (int) (color1.getGreen()*65535.0f);
            blue3 = (int) (color1.getBlue()*65535.0f);
          }
      }
    dx1 = x3-x1;
    dy1 = y3-y1;
    dz1 = z3-z1;
    if (dy1 == 0)
      return;
    dred1 = red3-red1;
    dgreen1 = green3-green1;
    dblue1 = blue3-blue1;
    dx2 = x2-x1;
    dy2 = y2-y1;
    dz2 = z2-z1;
    dred2 = red2-red1;
    dgreen2 = green2-green1;
    dblue2 = blue2-blue1;
    mx1 = dx1/dy1;
    mz1 = dz1/dy1;
    mred1 = dred1/dy1;
    mgreen1 = dgreen1/dy1;
    mblue1 = dblue1/dy1;
    xstart = xend = x1;
    zstart = zend = z1;
    redstart = redend = red1;
    greenstart = greenend = green1;
    bluestart = blueend = blue1;
    y = y1;
    if (dy2 != 0)
      {
        mx2 = dx2/dy2;
        mz2 = dz2/dy2;
        mred2 = dred2/dy2;
        mgreen2 = dgreen2/dy2;
        mblue2 = dblue2/dy2;
        if (y2 < 0)
          {
            xstart += mx1*dy2;
            xend += mx2*dy2;
            zstart += mz1*dy2;
            zend += mz2*dy2;
            redstart += mred1*dy2;
            redend += mred2*dy2;
            greenstart += mgreen1*dy2;
            greenend += mgreen2*dy2;
            bluestart += mblue1*dy2;
            blueend += mblue2*dy2;
            y = y2;
          }
        else if (y < 0)
          {
            xstart -= mx1*y;
            xend -= mx2*y;
            zstart -= mz1*y;
            zend -= mz2*y;
            redstart -= mred1*y;
            redend -= mred2*y;
            greenstart -= mgreen1*y;
            greenend -= mgreen2*y;
            bluestart -= mblue1*y;
            blueend -= mblue2*y;
            y = 0;
          }
        yend = (y2 < height ? y2 : height);
        index = y*width;
        while (y < yend)
          {
            if (xstart < xend)
              {
                left = xstart >> 16;
                right = xend >> 16;
                z = zstart;
                dz = zend-zstart;
                red = redstart;
                dred = redend-redstart;
                green = greenstart;
                dgreen = greenend-greenstart;
                blue = bluestart;
                dblue = blueend-bluestart;
              }
            else
              {
                left = xend >> 16;
                right = xstart >> 16;
                z = zend;
                dz = zstart-zend;
                red = redend;
                dred = redstart-redend;
                green = greenend;
                dgreen = greenstart-greenend;
                blue = blueend;
                dblue = bluestart-blueend;
              }
            if (left != right)
              {
                dz /= (right-left);
                dred /= (right-left);
                dgreen /= (right-left);
                dblue /= (right-left);
                if (left < 0)
                  {
                    z -= left*dz;
                    red -= left*dred;
                    green -= left*dgreen;
                    blue -= left*dblue;
                    left = 0;
                  }
                if (right > width)
                  right = width;
                for (i = left; i < right; i++)
                  {
                    if (z < zbuffer[index+i] && z > clipDist)
                      {
                        pixel[index+i] = 0xFF000000 + ((red & 0xFF00)<<8) + (green & 0xFF00) + (blue >> 8);
                        zbuffer[index+i] = z;
                      }
                    z += dz;
                    red += dred;
                    green += dgreen;
                    blue += dblue;
                  }
              }
            xstart += mx1;
            zstart += mz1;
            redstart += mred1;
            greenstart += mgreen1;
            bluestart += mblue1;
            xend += mx2;
            zend += mz2;
            redend += mred2;
            greenend += mgreen2;
            blueend += mblue2;
            index += width;
            y++;
          }
      }
    dx2 = x3-x2;
    dy2 = y3-y2;
    dz2 = z3-z2;
    dred2 = red3-red2;
    dgreen2 = green3-green2;
    dblue2 = blue3-blue2;
    if (dy2 != 0)
      {
        mx2 = dx2/dy2;
        mz2 = dz2/dy2;
        mred2 = dred2/dy2;
        mgreen2 = dgreen2/dy2;
        mblue2 = dblue2/dy2;
        xend = x2;
        zend = z2;
        redend = red2;
        greenend = green2;
        blueend = blue2;
        if (y < 0)
          {
            xstart -= mx1*y;
            xend -= mx2*y;
            zstart -= mz1*y;
            zend -= mz2*y;
            redstart -= mred1*y;
            redend -= mred2*y;
            greenstart -= mgreen1*y;
            greenend -= mgreen2*y;
            bluestart -= mblue1*y;
            blueend -= mblue2*y;
            y = 0;
          }
        yend = (y3 < height ? y3 : height);
        index = y*width;
        while (y < yend)
          {
            if (xstart < xend)
              {
                left = xstart >> 16;
                right = xend >> 16;
                z = zstart;
                dz = zend-zstart;
                red = redstart;
                dred = redend-redstart;
                green = greenstart;
                dgreen = greenend-greenstart;
                blue = bluestart;
                dblue = blueend-bluestart;
              }
            else
              {
                left = xend >> 16;
                right = xstart >> 16;
                z = zend;
                dz = zstart-zend;
                red = redend;
                dred = redstart-redend;
                green = greenend;
                dgreen = greenstart-greenend;
                blue = blueend;
                dblue = bluestart-blueend;
              }
            if (left != right)
              {
                dz /= (right-left);
                dred /= (right-left);
                dgreen /= (right-left);
                dblue /= (right-left);
                if (left < 0)
                  {
                    z -= left*dz;
                    red -= left*dred;
                    green -= left*dgreen;
                    blue -= left*dblue;
                    left = 0;
                  }
                if (right > width)
                  right = width;
                for (i = left; i < right; i++)
                  {
                    if (z < zbuffer[index+i] && z > clipDist)
                      {
                        pixel[index+i] = 0xFF000000 + ((red & 0xFF00)<<8) + (green & 0xFF00) + (blue >> 8);
                        zbuffer[index+i] = z;
                      }
                    z += dz;
                    red += dred;
                    green += dgreen;
                    blue += dblue;
                  }
              }
            xstart += mx1;
            zstart += mz1;
            redstart += mred1;
            greenstart += mgreen1;
            bluestart += mblue1;
            xend += mx2;
            zend += mz2;
            redend += mred2;
            greenend += mgreen2;
            blueend += mblue2;
            index += width;
            y++;
          }
      }
  }
}